#!/usr/bin/env python
# -*- coding:utf-8 -*-

import socket
import logging,socket,threading,datetime

"""
s = socket.socket()  # 创建socket对象
s.bind(('127.0.0.1',9999))   # 一个二元组
s.listen()  #开始监听
# 开启一个连接
s1,info = s.accept() 
# 阻塞直到和客户端成功建立连接,返回一个socket对象和客户端地址.

# 使用缓冲区获取数据
data = s1.recv(1024)
print(data,info)
s1.send(b'magedu.com ack')

# 开启另外一个连接
s2, _ = s.accept()

data = s2.recv(1024)
s2.send(b'hello python')
s.close()
# 上例accept和recv是阻塞的,主线程经常被阻塞住而不能工作.
"""




# 练习 ---- 写一个群聊程序
"""
需求分析:
聊天工具是CS程序,C是每一个客户端,S是服务器端.
服务器应该具有的功能:
    启动服务,包括绑定地址和端口,监听.
    建立连接,能和多个客户端建立连接.
    接收不同用户的信息.
    分发,将接收的某个用户的信息转发到已连接的所有客户端.
    停止服务.
    记录连接的客户端.
"""


# 代码实现
# 服务端应该对应一个类
"""
logging.basicConfig(level=logging.INFO,format="%(asctime)s %(thread)d %(message)s")

class ChatServer:
    def __init__(self,ip='127.0.0.1',port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip,port)
        self.clients = {} # 客户端


    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while True:
            sock, client = self.sock.accept() # 阻塞
            self.clients[client] = sock # 添加到客户端字典
            # 准备接收数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv,args=(sock,client)).start()



    def recv(self): # 接收客户端数据
        while True:
            data = sock.recv(1024) # 阻塞到数据到来.
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(),*client,data.decode())
            logging.info(msg)
            msg = msg.encode()
            for s in self.clients.values():
                s.send(msg)


    def stop(self): # 停止服务
        for s in self.clients.values():
            s.close()
        self.sock.close()

cs = ChatServer()
cs.start()
"""





# 基本功能完成,但是有问题,使用Event改进.
"""
logging.basicConfig(level=logging.INFO,format="%(asctime)s %(thread)d %(message)s")

class ChatServer:
    def __init__(self,ip='127.0.0.1',port=9999): # 启动服务.
        self.sock = socket.socket()
        self.addr = (ip,port)
        self.clients = {} # 客户端
        self.event = threading.Event()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新线程.
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock,client = self.sock.accept() # 阻塞
            self.clients[client] = sock # 添加到客户端字典
            # 准备接收数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv,args=(sock,client)).start()

    def recv(self,sock:socket.socket,client): # 接收客户端数据
        while not self.event.is_set():
            data = sock.recv(1024) # 阻塞到数据到来
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(),*client,data.decode())
            logging.info(msg)
            msg = msg.encode()
            for s in self.clients.values():
                s.send(msg)

    def stop(self):  # 停止服务
        for s in self.clients.values():
            s.close()
        self.sock.close()
        self.event.set()

cs = ChatServer()
cs.start()

while True:
    cmd = input('>>>').strip()
    if cmd == 'quit':
        cs.stop()
        threading.Event().wait(3)
        break
"""
"""
基本能用,测试通过.
完善的地方: 例如各种异常的判断,客户端断开连接后字典中的移除客户端数据等.

客户端主动断开带来的问题
   服务端知道自己何时断开,如果客户端断开,服务器不知道.
   所以,好的做法是,客户端断开发出特殊消息通知服务器端断开连接. 
   但是,如果客户端主动断开,服务端主动发送一个空消息,超时返回异常,捕获异常并清理连接.
   即使为客户端提供了断开命令,也不能保证客户端会使用它断开连接. 但是还是要增加这个退出功能.
"""





# 增加客户端退出命令
"""
logging.basicConfig(level=logging.INFO,format="%(asctime)s %(thread)d %(message)s")

class ChatServer:
    def __init__(self,ip='127.0.0.1',port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip,port)
        self.clients = {} # 客户端
        self.event = threading.Event()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock,client = self.sock.accept() # 阻塞
            self.clients[client] = sock # 添加到客户端字典
            # 准备接收数据,recv是阻塞的,开启新的线程
            threading.Thread(target=self.recv,args=(sock,client)).start()

    def recv(self,sock:socket.socket,client): # 接收客户端数据
        while not self.event.is_set():
            data = sock.recv(1024) # 阻塞到数据到来
            msg = data.decode().strip()
            # 客户端退出命令
            if msg == 'quit':
                self.clients.pop(client)
                sock.close()
                logging.info('{} quits'.format(client))
                break
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(),*client,data.decode())
            logging.info(msg)
            msg = msg.encode()
            for s in self.clients.values():
                s.send(msg)

    def stop(self): # 停止服务
        for s in self.clients.values():
            s.close()
        self.sock.close()
        self.event.set()

cs = ChatServer()
cs.start()

while True:
    cmd = input('>>>').strip()
    if cmd == 'quit':
        cs.stop()
        threading.Event().wait(3)
        break
    logging.info(threading.enumerate())  # 用来观察断开后线程的变化.
# 业务功能基本完成了.
"""





# MakeFile
# socket.makefile(mode='r',buffering=None,*,encoding=None, errors=None,newline=None)
# 创建一个与该套接字相关联的文件对象,将recv方法看做读方法,将send方法看做写方法.
# 使用makefile简单例子
"""
import socket

sockserver = socket.socket()
ip = '127.0.0.1'
port=9999
addr = (ip,port)
sockserver.bind(addr)
sockserver.listen()
print('-'*30)
s,_ = sockserver.accept()
print('-'*30)
f = s.makefile(mode='rw')

line = f.read(10) # 阻塞等.
print('-'*30)
print(line)
f.write('Return your msg: {}'.format(line))
f.flush()
"""




# 上例不能循环接收消息,修改一下
"""
import socket,threading

sockserver = socket.socket()
ip = '127.0.0.1'
port = 9999
addr = (ip,port)
sockserver.bind(addr)
sockserver.listen()
print('-'*30)

event = threading.Event()

def accept(sock:socket.socket,e:threading.Event):
    s,_ = sock.accept()
    f = s.makefile(mode='rw')

    while True:
        line = f.readline()
        print(line)
        if line.strip() == "quit": # 注意要发quit\n
            break
        f.write('Return your msg: {}'.format(line))
        f.flush()
    f.close()
    sock.close()
    e.wait(3)


t = threading.Thread(target=accept,args=(sockserver,event))
t.start()
t.join()

print(sockserver)
"""





# makefile练习
# 使用makefile改写群聊类
"""
import logging,socket,threading,datetime

logging.basicConfig(level=logging.INFO,format="%(asctime)s %(thread)d %(message)s")

class ChatServer:
    def __init__(self,ip='127.0.0.1',port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip,port)
        self.clients = {} # 客户端
        self.event = threading.Event()

def start(self): # 启动监听
    self.sock.bind(self.addr)  # 绑定
    self.sock.listen() # 监听
    # accept会阻塞主线程,所以开一个新线程
    threading.Thread(target=self.accept).start()

def accept(self): # 多人连接
    while not self.event.is_set():
        sock,client = self.sock.accept() # 阻塞
        # 准备接收数据,recv是阻塞的,开启新的线程
        f = sock.makefile('rw') # 支持读写
        self.clients[client] = f # 添加到客户端字典.
        threading.Thread(target=self.recv,args=(f,client),name='recv').start()

def recv(self,f,client): # 接收客户端数据
    while not self.event.is_set():
        data = f.readline()  # 阻塞到换行符
        msg = data.strip()
        # 客户端退出命令
        if msg == 'quit':
            self.clients.pop(client)
            f.close()
            logging.info('{} quits'.format(client))
            break
        msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(),*client,data)

        logging.info(msg)

        for s in self.clients.values():
            s.write(msg)
            s.flush()

def stop(self): # 停止服务
    for s in self.clients.values():
        s.close()
    self.sock.close()
    self.event.set()

cs = ChatServer()
cs.start()

while True:
    cmd = input('>>>').strip()
    if cmd == 'quit':
        cs.stop()
        threading.Event().wait(3)
        break
    logging.info(threading.enumerate()) # 用来观察断开后线程的变化.
"""
"""
上例完成了基本功能,但是,如果客户端主动断开,或者readline出现异常,就不会从clients中移除作废的socket。
可以使用异常处理解决这个问题.
"""




# ChatServer实验用完整代码
# 注意:仅作为实验用. Socket太底层了,实际开发中很少使用这么底层的接口.增加了一些异常处理.
"""
import logging,socket,threading,datetime

logging.basicConfig(level=logging.INFO,format="%(asctime)s %(thread)d %(message)s")

class ChatServer:
    def __init__(self,ip='127.0.0.1',port=9999): # 启动服务
        self.sock = socket.socket()
        self.addr = (ip,port)
        self.clients = {} # 客户端
        self.event = threading.Event()

    def start(self): # 启动监听
        self.sock.bind(self.addr) # 绑定
        self.sock.listen() # 监听
        # accept会阻塞主线程,所以开一个新线程
        threading.Thread(target=self.accept).start()

    def accept(self): # 多人连接
        while not self.event.is_set():
            sock,client = self.sock.accept() # 阻塞
            # 准备接收数据, recv是阻塞的, 开启新的线程
            f = sock.makefile('rw') # 支持读写
            self.clients[client] = f # 添加到客户端字典
            threading.Thread(target=self.recv,args=(f,client),name='recv').start()

    def recv(self,f,client): # 接收客户端数据
        while not self.event.is_set():
            try:
                data = f.readline() # 阻塞到换行符
            except Exception as e:
                logging.error(e) # 有任何异常,退出
                data = 'quit'
                msg = data.strip()
            # 客户端退出命令
            if msg == 'quit':
                self.clients.pop(client)
                f.close()
                logging.info('{} quits'.format(client))
                break
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(),*client,data)
            logging.info(msg)

            for s in self.clients.values():
                s.write(msg)
                s.flush()

        
    def stop(self): #停止服务
        for s in self.clients.values():
            s.close()
        self.sock.close()
        self.event.set()

def main():
    cs = ChatServer()
    cs.start()

    while True:
        cmd = input('>>>').strip()
        if cmd == 'quit':
            cs.stop()
            threading.Event().wait(3)
            break
        logging.info(threading.enumerate()) # 用来观察断开后线程的变化
    
if __name__=='__main__':
    main()
"""




"""
TCP客户端编程
客户端编程步骤

- 创建Socket对象
- 连接到远端服务端的ip和port, connect()方法
- 传输数据
  - 使用send,recv方法发送,接收数据
- 关闭连接,释放资源.
"""
"""
import socket

client = socket.socket()
ipaddr = ('127.0.0.1',9999)
client.connect(ipaddr) # 直接连接服务器

client.send(b'abcd\n')
data = client.recv(1024) # 阻塞等待
print(data)

client.close()
"""




# 开始编写客户端类
import socket,threading,datetime,logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO)

class ChatClient:
    def __init__(self,ip='127.0.0.1',port=9999):
        self.sock = socket.socket()
        self.addr = (ip,port)
        self.event = threading.Event()

    def start(self): # 启动对远程服务器的连接
        self.sock.connect(self.addr)
        self.send("I'm ready.")
        # 准备接收数据,recv是阻塞的,开启新的线程
        threading.Thread(target=self.recv,name="recv").start()

    def recv(self): # 接收客户端的数据
        while not self.event.is_set():
            try:
                data = self.sock.recv(1024) # 阻塞
            except Exception as e:
                logging.error(e)
                break
            msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(),*self.addr,data.strip())
            logging.info(msg)

    def send(self,msg:str):
        data = "{}\n".format(msg.strip()).encode() # 服务端需要一个换行符.
        self.sock.send(data)

    def stop(self):
        self.sock.close()
        self.event.wait(3)
        self.event.set()
        logging.info('Client stops.')

def main():
    cc = ChatClient()
    cc.start()
    while True:
        cmd = input('>>>')
        if cmd.strip() == 'quit':
            cc.stop()
            break
        cc.send(cmd) # 发送消息

if __name__=='__main__':
    main()


